package org.melanesia.sql;

import java.sql.BatchUpdateException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import org.melanesia.conf.Config;
import org.melanesia.converters.JavaToJdbcConverter;
import org.melanesia.converters.TypeConverter;
import org.melanesia.sql.exceptions.BatchExecutionException;
import org.melanesia.sql.exceptions.EmptyBatchException;
import org.melanesia.sql.exceptions.SQLExecutionException;
import org.melanesia.sql.utils.JDBCUtils;

/**
 * Implements batch sql processing similar to the one described in: -
 * {@link java.sql.PreparedStatement#addBatch()} -
 * {@link java.sql.PreparedStatement#executeBatch()} - etc.
 *
 * The batch can contain multiple sets of parameters, which share the same, or
 * SQL clause. This constructs allows (if implemented in jdbc driver of course)
 * great speed up in cases when one has to execut a large amount of database
 * inserts/updates.
 *
 * Each element in the batch must be added either by {@link #addBatch()}.
 * Note, that every batch MUST be started with {@link Batch#addBatch()}.
 *
 * Example:
 *
 * <pre>
 * {@code
 *   sqlBuilder.createBatch("INSERT :ID, :NAME, :SURNAME, :SEX INTO PERSONS")
 *
 *     .addBatch()
 *     .setInputParameter("ID", 10)
 *     .setInputParameter("NAME", "John")
 *     .setInputParameter("SURNAME", "Doe")
 *     .setInputParameter("SEX", "M")
 *
 *     .addBatch()
 *     .setInputParameter("ID", 20)
 *     .setInputParameter("NAME", "Jane")
 *     .setInputParameter("SURNAME", "Doe")
 *     .setInputParameter("SEX", "F")
 *
 *      //...or define its own.
 *     .addBatch()
 *     .setInputParameter("ID", 30)
 *     .setInputParameter...
 *
 *      // In the end, just execute the batch
 *     .execute();
 * }
 * </pre>
 *
 * @author marcin.kielar
 */
public final class Batch extends AbstractSQL {

    /** Logger. */
    private static Logger logger = Logger.getLogger(Batch.class.getName());

    /** Query metadata list - parameters, converters, etc. - for every batch. */
    private final List<QueryMetaData> queryMetaDataList = new ArrayList<QueryMetaData>();

    /**
     * Current QueryMetaData in the {@link #queryMetaDataList}. Initially its
     * value is null, and one has to call {@link #addBatch()} in order to add
     * parameters to the batch.
     */
    private QueryMetaData currentQueryMetaData;

    /**
     * Creates new <code>Batch</code>.
     *
     * @param connection
     *            jdbc connection
     * @param typeConverter
     *            type converter
     * @param sql
     *            sql query to batch
     */
    Batch(final Connection connection, final TypeConverter typeConverter, final String sql) {
        super(connection, typeConverter, sql);
    }

    /**
     * Adds new parameter set for the query in this batch.
     *
     * @return this batch
     */
    public Batch addBatch() {

        currentQueryMetaData = new QueryMetaData();
        queryMetaDataList.add(currentQueryMetaData);

        return this;
    }

    /**
     * Checks if current batch exists. This method is used to check if
     * {@link #addBatch()} has been called at least once, before
     * {@link #setParameter(String, Object)}, etc. are used.
     */
    private void checkCurrentBatch() {
        if (currentQueryMetaData == null) {
            throw new EmptyBatchException();
        }
    }

    /**
     * Sets query parameter for current element.
     *
     * @param name
     *            parameter name
     * @param value
     *            parameter value
     * @return this batch
     */
    public Batch setParameter(final String name, final Object value) {

        checkCurrentBatch();

        currentQueryMetaData.setInputParameter(name, value);
        return this;
    }

    /**
     * Sets query parameter and java to jdbc type converter for current element.
     *
     * @param name
     *            parameter name
     * @param value
     *            parameter value
     * @param converter
     *            java to jdbc converter
     * @return this batch
     */
    public Batch setParameter(final String name, final Object value, final JavaToJdbcConverter converter) {

        checkCurrentBatch();

        currentQueryMetaData.setInputParameter(name, value, converter);
        return this;
    }

    /**
     * Executes the batch and returns update count array as described in
     * {@link java.sql.Statement#executeBatch()}.
     *
     * @return update count array
     *
     * @see java.sql.Statement#executeBatch()
     */
    public BatchResult execute() {

        PreparedStatement stmt = null;
        try {

            if (Config.isDebugMode()) {
                logger.info("Executing batch:\n" + getSql());
            }

            // We are using prepared statement here, to get full performance on JDBC drivers (like Oracle).
            // Note, that this is only possible with single SQL statement, and Batch does not support multiple sql in single batch object.
            stmt = getConnection().prepareStatement(getSql());

            for (QueryMetaData queryMetaData : queryMetaDataList) {

                // query parameters
                for (String parameterName : queryMetaData.getInputParameterNames()) {
                    InputParameter parameter = queryMetaData.getInputParameter(parameterName);
                    bindInputParameter(stmt, parameter);
                }

                stmt.addBatch();
            }

            int[] updateCounts = null;
            Iterator<Throwable> errorCausesIterator = null;

            try {

               updateCounts = stmt.executeBatch();

            } catch (BatchUpdateException ex) {
                updateCounts = ex.getUpdateCounts();
                errorCausesIterator = ex.iterator();
            }

            BatchResult batchResult = new BatchResult(this.size(), updateCounts, errorCausesIterator);

            if (batchResult.isFailed()) {
                throw new BatchExecutionException("Error executing batch.", batchResult);
            }

            return batchResult;

        } catch (BatchExecutionException ex) {
            throw ex;
        } catch (Throwable t) {
            throw new SQLExecutionException(t);
        } finally {
            queryMetaDataList.clear();
            JDBCUtils.close(stmt);
        }
    }

    /**
     * Returns this batch size, ie. the numer of elements in this batch.
     *
     * @return number of elements in this batch
     */
    public int size() {
        return queryMetaDataList.size();
    }
}
